﻿using System;
using System.Data.Objects;
using System.Threading;

namespace EFObjectContextScope
{
    public enum ObjectContextScopeOption
    {
        Required,

        RequiresNew
    }

    public class ObjectContextScope<TObjectContext> : IDisposable
                                where TObjectContext : ObjectContext
    {
        internal ObjectContextWrapper<TObjectContext> contextObjectContextWrapper;
        internal ObjectContextWrapper<TObjectContext> parentContextObjectContextWrapper;
        internal ObjectContextScope<TObjectContext> parentCurrentScope;
        internal ObjectContextData<TObjectContext> threadContextData;

        private Thread scopeThread;

        internal bool disposed = false;

        private static object threadSync = new object();

        public ObjectContextScope()
            : this(ObjectContextScopeOption.Required)
        {
        }

        public ObjectContextScope(ObjectContextScopeOption scopeOption)
        {
            lock (threadSync)
            {
                if (this.NeedToCreateWrapper(scopeOption))
                {
                    this.contextObjectContextWrapper = new ObjectContextWrapper<TObjectContext>();

                    //System.Diagnostics.Debug.WriteLine(string.Format(CultureInfo.InvariantCulture,
                    //                                                 "ObjectContextScope no ambient scope, creating {0}",
                    //                                                 this.contextObjectContextWrapper.
                    //                                                     ObjectContextTraceId.
                    //                                                     ObjectContextWrapperIdentifier));
                    this.PushScope();
                }
                else
                {
                    this.contextObjectContextWrapper = contextObjectContextWrapper;
                    Interlocked.Increment(ref this.contextObjectContextWrapper.cloneCount);
                    this.contextObjectContextWrapper.cloneId = this.contextObjectContextWrapper.cloneCount;

                    //System.Diagnostics.Debug.WriteLine(string.Format(CultureInfo.InvariantCulture,
                    //                                                 "ObjectContextScope ambient scope present, using {0}",
                    //                                                 this.contextObjectContextWrapper.
                    //                                                     ObjectContextTraceId.
                    //                                                     ObjectContextWrapperIdentifier));
                }
            }
        }

        private void PushScope()
        {
            this.threadContextData.CurrentObjectContextWrapper = this.contextObjectContextWrapper;
            this.threadContextData.CurrentScope = this;
        }

        //private void PopScope()
        //{
        //    this.threadContextData.CurrentScope = this.parentCurrentScope;
        //    this.threadContextData.CurrentObjectContextWrapper = this.contextObjectContextWrapper;
        //}

        /// <exception cref="ArgumentOutOfRangeException"><c>scopeOption</c> is out of range.</exception>
        private bool NeedToCreateWrapper(ObjectContextScopeOption scopeOption)
        {
            bool flag = false;
            this.CommonInitialize();
            switch (scopeOption)
            {
                case ObjectContextScopeOption.Required:
                    this.contextObjectContextWrapper = this.parentContextObjectContextWrapper;
                    if (this.contextObjectContextWrapper == null)
                    {
                        flag = true;
                    }
                    return flag;

                case ObjectContextScopeOption.RequiresNew:
                    return true;

            }
            throw new ArgumentOutOfRangeException("scopeOption");
        }

        private void CommonInitialize()
        {
            this.disposed = false;
            this.contextObjectContextWrapper = null;
            this.scopeThread = Thread.CurrentThread;

            ObjectContextWrapper<TObjectContext>.GetCurrentWrapperAndScope(
                out this.parentContextObjectContextWrapper, out this.parentCurrentScope,
                out this.threadContextData, out this.contextObjectContextWrapper);
        }

        public TObjectContext CurrentObjectContext
        {
            get
            {
                return this.contextObjectContextWrapper.objectContext;
            }
        }

        #region IDisposable Members

        /// <summary>
        /// Finalizes an instance of the <c>ObjectContextScope</c> class.
        /// <para>
        /// I have added this on to clean up unmanaged resources
        /// Microsoft recommend doing this even though unmanaged code should not be coming though this class.
        /// </para>
        /// <para>
        /// Do not re-create Dispose clean-up code here.
        /// Calling Dispose(false) is optimal in terms of
        /// readability and maintainability.
        /// </para>
        /// </summary>
        ~ObjectContextScope()
        {
            Dispose(false);
        }

        /// <summary>
        /// Implement <see cref="IDisposable"/>.
        /// Do not make this method virtual.
        /// A derived class should not be able to override this method.
        /// See: http://msdn.microsoft.com/en-us/library/system.idisposable.dispose.aspx
        /// </summary>
        public void Dispose()
        {
            Dispose(true);

            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        #endregion


        #region IDisposable Members

        /// <summary>
        /// Dispose(<c>bool</c> disposing) executes in two distinct scenarios. If disposing
        /// equals true, the method has been called directly or indirectly by a user's
        /// code. Managed and unmanaged resources can be <see cref="disposed"/>. If
        /// disposing equals false, the method has been called by the runtime from inside
        /// the <c>finalizer</c> and you should not reference other objects. Only unmanaged
        /// resources can be <see cref="disposed"/>.
        /// </summary>
        /// <param name="disposing">Dispose of managed objects?</param>
        /// <exception cref="InvalidOperationException"><c>ObjectContextScope</c>
        /// cannot be disposed of if the parent scope is the same</exception>
        private void Dispose(bool disposing)
        {
            if (disposed)
            {
                return;
            }

            // If disposing equals true, dispose all managed
            // and unmanaged resources.
            if (disposing)
            {
                if (this.scopeThread != Thread.CurrentThread)
                {
                    throw new InvalidOperationException("InvalidScopeThread");
                }

                Exception exception = null;

                try
                {
                    this.disposed = true;

                    ObjectContextScope<TObjectContext> currentScope = this.threadContextData.CurrentScope;

                    if (!this.Equals(currentScope))
                    {
                        if (currentScope == null)
                        {
                            throw new InvalidOperationException("ObjectContextScopeInvalidScopeNesting");
                        }
                    }
                    else
                    {
                        if ((parentContextObjectContextWrapper != null) || (parentCurrentScope != null))
                        {
                            throw new InvalidOperationException("Scope Nesting Incorrect");
                        }
                        contextObjectContextWrapper.Dispose();
                        contextObjectContextWrapper = null;
                        this.threadContextData.Dispose();
                        this.threadContextData = null;
                    }
                }
                finally
                {
                }
                if (exception != null)
                {
                    throw exception;
                }
            }

            // disposing has been done.
            //Console.WriteLine(string.Format("Scope {0} Disposed", this.contextObjectContextWrapper.ObjectContextTraceId.ObjectContextWrapperIdentifier));
        }

        #endregion
    }
}
